import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;

import java.util.concurrent.LinkedBlockingQueue;

import global.PageId;
import diskmgr.DB;
import diskmgr.Page;
import exceptions.DiskMgrException;
import exceptions.FileIOException;
import exceptions.InvalidPageNumberException;

import exceptions.InvalidRunSizeException;
import exceptions.OutOfSpaceException;

import exceptions.InvalidRunSizeException;
import exceptions.PageUnpinnedException;

import HashTable.MyHashTable;

public class BuffMgr {

	private Descriptor[] desArray;
	private MyHashTable<Integer, LinkedList<Integer>> hash = new MyHashTable<Integer, LinkedList<Integer>>();
	private Queue<Integer> emptyFrams = new LinkedBlockingQueue<Integer>();
	private DB myData = new DB();

	// open the data base
	public BuffMgr(int numbufs, String replacerArg) {

		// try synch
		desArray = new Descriptor[numbufs];
		// open data base

	}

	/**
	 * Pin a page. First check if this page is already in the buffer pool. If it
	 * is, increment the pin_count and return a pointer to this page. If the
	 * pin_count was 0 before the call, the page was a replacement candidate,
	 * but is no longer a candidate. If the page is not in the pool, choose a
	 * frame (from the set of replacement candidates) to hold this page, read
	 * the page (using the appropriate method from {\em diskmgr} package) and
	 * pin it. Also, must write out the old page in chosen frame if it is dirty
	 * before reading new page. (You can assume that emptyPage==false for this
	 * assignment.)
	 * 
	 * @param Page_Id_in_a_DB
	 *            page number in the minibase.
	 * @param page
	 *            the pointer poit to the page.
	 * @param emptyPage
	 *            true (empty page); false (non-empty page)
	 * @throws IOException
	 * @throws FileIOException
	 * @throws InvalidPageNumberException
	 */
	public void pinPage(PageId pin_pgid, Page page, boolean emptyPage)
			throws InvalidPageNumberException, FileIOException, IOException {

		LinkedList<Integer> frameNumbers = null;
		
		Descriptor returnedPage;
		
		int frameNumber = -1;
		// get the frame number

		try {
			frameNumber = searchThePage(pin_pgid);

			if (frameNumber != -1) {
				// this case means we find the frame number corresponding to
				// page number
				returnedPage = desArray[frameNumber];
				returnedPage.setPinCount(returnedPage.getPinCount() + 1);
				desArray[frameNumber]=returnedPage;			
				Iterator<Integer> itQ  = emptyFrams.iterator();
				while(itQ.hasNext()){
					int curr = itQ.next();
					if(frameNumber==curr){
						itQ.remove();
						break;
					}
				}
			} else {
				// this case means we find the frame number with a different
				// page number ( Collision )
				// so we added a new frame number to the linked list
				// must create a new page as it's not in the pool
				frameNumbers = (LinkedList<Integer>) hash
						.get(pin_pgid.getPid());
				try{
				frameNumber = emptyFrams.poll();
				}catch(Exception e){
				throw new RuntimeException("No Frames");	
				// needs to be revised 
				}
				frameNumbers.add(frameNumber);
				Descriptor lastPage = desArray[frameNumber];
				if(lastPage.isDirtyBit()){
					Page p = new Page();
					myData.write_page(lastPage.getPageNumber(),p);
				}
				
				frameNumbers.add(frameNumber);

				myData.read_page(pin_pgid, page);
				Descriptor de = new Descriptor(1, false, pin_pgid);
				desArray[frameNumber] = de;
				
			
			}

		}
		// frame number is n't exist
		catch (Exception e) {
			// this case means the hash table with the hash function is empty
			// we must create a new frame number with a new linked list
			// must creat a new page as it's not in the pool
			LinkedList<Integer> frameNum = new LinkedList<Integer>();
			 frameNumber = emptyFrams.poll();
			Descriptor lastPage = desArray[frameNumber];
			if(lastPage.isDirtyBit()){
				Page p = new Page();
				myData.write_page(lastPage.getPageNumber(),p);
			}
			frameNum.add(frameNumber);
			myData.read_page(pin_pgid, page);
			hash.put(pin_pgid.getPid(), frameNum);
			Descriptor de = new Descriptor(1, false, pin_pgid);
			desArray[frameNumber] = de;
		}

		// page is already exist

	};

	/**
	 * 
	 * return -1 if page not found
	 * 
	 * @throws Exception
	 * 
	 * @return frame number
	 */
	private int searchThePage(PageId pin_pgid) {
		LinkedList<Integer> frameNumbers = null;
		int frameNumber = -1;
		Descriptor returnedPage;
		int returnedPageNumber;
		try {
			frameNumbers = (LinkedList<Integer>) hash.get(pin_pgid.getPid());
		} catch (Exception e) {
			throw new RuntimeException("Not found in hash");
		}
		Iterator<Integer> it = frameNumbers.iterator();
		while (it.hasNext()) {
			frameNumber = it.next();
			returnedPage = desArray[frameNumber];
			returnedPageNumber = returnedPage.getPageNumber().getPid();
			if (pin_pgid.getPid() == returnedPageNumber) {
				return frameNumber;
			}

		}
		return -1;
	}

	/**
	 * Unpin a page specified by a pageId. This method should be called with
	 * dirty==true if the client has modified the page. If so, this call should
	 * set the dirty bit for this frame. Further, if pin_count>0, this method
	 * should decrement it. If pin_count=0 before this call, throw an exception
	 * to report error. (For testing purposes, we ask you to throw an exception
	 * named PageUnpinnedException in case of error.)
	 * 
	 * @param globalPageId_in_a_DB
	 *            page number in the minibase.
	 * @param dirty
	 *            the dirty bit of the frame
	 * @throws PageUnpinnedException 
	 */
	public void unpinPage(PageId PageId_in_a_DB, boolean dirty) throws PageUnpinnedException {
		// if pin count > 0 then decrement it and set the dirty bit if any changes happened
		//else throw exception "page not found || unPin page with pin count =0
        try{
        	int frameNumber = searchThePage(PageId_in_a_DB);
        	if(frameNumber!=-1)
        	{
        		Descriptor requiredPage = desArray[frameNumber];
        		int pinsNumber = requiredPage.getPinCount();
        		// user finish using the page
        		if(pinsNumber>0)
        		{
        			if(dirty)
        			{
        				requiredPage.setDirtyBit(true);
        			}
        			if(pinsNumber==1)
        			{
        				emptyFrams.add(PageId_in_a_DB.getPid());
        			}
        			requiredPage.setPinCount(pinsNumber-1);
        		}
        		else
        		{    // unpin for not using page 
        			throw new PageUnpinnedException(null,"No one Using it");
        		}
        	}
        	else
        	{
        		// not found in the Linked List
        		throw new PageUnpinnedException(null,"Not found in hash");
        	}
        }catch(Exception e ){
        	//not found in the Hash
        	throw new PageUnpinnedException(null,"Not found in hash");
        }
        
	};

	/**
	 * Allocate new pages. Call DB object to allocate a run of new pages and
	 * find a frame in the buffer pool for the first page and pin it. (This call
	 * allows a client of the Buffer Manager to allocate pages on disk.) If
	 * buffer is full, i.e., you canÂ’t find a frame for the first page, ask DB
	 * to deallocate all these pages, and return null.
	 * 
	 * @param firstpage
	 *            the address of the first page.
	 * @param howmany
	 *            total number of allocated new pages.
	 * 
	 * @return the first page id of the new pages. null, if error.
	 * @throws IOException 
	 * @throws DiskMgrException 
	 * @throws FileIOException 
	 * @throws InvalidPageNumberException 
	 * @throws InvalidRunSizeException 
	 * @throws OutOfSpaceException 
	 */
	public PageId newPage(Page firstpage, int howmany) throws OutOfSpaceException, InvalidRunSizeException, InvalidPageNumberException, FileIOException, DiskMgrException, IOException {
		PageId p = new PageId();
		myData.allocate_page(p,howmany);
		try{
		pinPage(p, firstpage, false);
		return p;
		}catch(Exception e){
			myData.deallocate_page(p,howmany );
		}
		return null;
		
	};

	/**
	 * Used to flush a particular page of the buffer pool to disk. This method
	 * calls the write_page method of the diskmgr package.
	 * 
	 * @param pageid
	 *            the page number in the database.
	 * @throws PageUnpinnedException 
	 */
	public void flushPage(PageId pageid) throws PageUnpinnedException {

		try{
        	int frameNumber = searchThePage(pageid);
        	if(frameNumber!=-1)
        	{
        		Descriptor requiredPage = desArray[frameNumber];
        		int pinsNumber = requiredPage.getPinCount();
        		// user finish using the page
        		if(pinsNumber>0)
         		{
        			// which page should i write ?
        			Page p = new Page();
        			myData. write_page(pageid, p);
        		}
        		else
        		{    // unpin for not using page 
        			throw new PageUnpinnedException(null,"No one Using it");
        		}
        	}
        	else
        	{
        		// not found in the Linked List
        		throw new PageUnpinnedException(null,"Not found in hash");
        	}
        }catch(Exception e ){
        	//not found in the Hash
        	throw new PageUnpinnedException(null,"Not found in hash");
        }
	};

	/**
	 * This method should be called to delete a page that is on disk. This
	 * routine must call the method in diskmgr package to deallocate the page.
	 * 
	 * @param globalPageId
	 *            the page number in the data base.
	 * @throws IOException
	 * @throws DiskMgrException
	 * @throws FileIOException
	 * @throws InvalidPageNumberException
	 * @throws InvalidRunSizeException
	 */
	public void freePage(PageId globalPageId) throws InvalidRunSizeException,
			InvalidPageNumberException, FileIOException, DiskMgrException,
			IOException {
	//	boolean check= false;
	LinkedList<Integer> frameNums=	(LinkedList<Integer>) hash.get(globalPageId.getPid());
		Iterator<Integer> it = frameNums.iterator();
		int frameNum = 0;
		try{
			frameNum = searchThePage(globalPageId);
			if(frameNum!=-1){
				Descriptor de = desArray[frameNum];
				if(de.getPinCount()==1){
					myData.deallocate_page(globalPageId);
					
					emptyFrams.add(frameNum);
				}else{
					throw new RuntimeException("can't be deleted");
				}
			}
		}catch (Exception e){
			
		}
//		while(it.hasNext()){
//			int frameNum = it.next();
//			Descriptor de = desArray[frameNum];
//			int returnedPageNumber = de.getPageNumber().getPid();
//			if(returnedPageNumber==globalPageId.getPid()){
//				myData.deallocate_page(globalPageId);
//				check=true;
//				break;
//			}
//			
//		}
		
	};
	public static void main(String[] args) throws OutOfSpaceException, InvalidRunSizeException, InvalidPageNumberException, FileIOException, DiskMgrException, IOException {
		Integer[] a = {1,2,3,4,5};
		Integer x = a[1];
		x=x+5;
		
		DB myData = new DB();
	
		myData.openDB("test");
			
		PageId p = new PageId();
		
		System.out.println(p.pid);
		myData.allocate_page(p,1);
		System.out.println(p.pid);
		myData.closeDB();
		
	}

}
